package com.snpic.appaw.module.insure.service.oauth2;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.snpic.appaw.framework.common.enums.UserTypeEnum;
import com.snpic.appaw.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.snpic.appaw.framework.common.pojo.PageResult;
import com.snpic.appaw.framework.common.util.date.DateUtils;
import com.snpic.appaw.framework.common.util.object.BeanUtils;
import com.snpic.appaw.framework.security.core.LoginUser;
import com.snpic.appaw.framework.tenant.core.context.TenantContextHolder;
import com.snpic.appaw.framework.tenant.core.util.TenantUtils;
import com.snpic.appaw.module.insure.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
import com.snpic.appaw.module.insure.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.snpic.appaw.module.insure.dal.dataobject.oauth2.OAuth2ClientDO;
import com.snpic.appaw.module.insure.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
import com.snpic.appaw.module.insure.dal.dataobject.user.AdminUserDO;
import com.snpic.appaw.module.insure.dal.mysql.oauth2.OAuth2AccessTokenMapper;
import com.snpic.appaw.module.insure.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
import com.snpic.appaw.module.insure.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
import com.snpic.appaw.module.insure.service.user.AdminUserService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.snpic.appaw.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static com.snpic.appaw.framework.common.util.collection.CollectionUtils.convertSet;

/**
 * OAuth2.0 Token Service 实现类
 *
 * @author 芋道源码
 */
@Service
public class OAuth2TokenServiceImpl implements OAuth2TokenService {

	@Resource
	private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
	@Resource
	private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;

	@Resource
	private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;

	@Resource
	private OAuth2ClientService oauth2ClientService;
	@Resource
	@Lazy // 懒加载，避免循环依赖
	private AdminUserService adminUserService;

	@Override
	@Transactional(rollbackFor = Exception.class)
	public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
		OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
		// 创建刷新令牌
		OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
		// 创建访问令牌
		return createOAuth2AccessToken(refreshTokenDO, clientDO);
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
		// 查询访问令牌
		OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
		if (refreshTokenDO == null) {
			throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌");
		}

		// 校验 Client 匹配
		OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
		if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) {
			throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确");
		}

        // 移除相关的访问令牌
        List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
        if (CollUtil.isNotEmpty(accessTokenDOs)) {
            oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
            oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
        }

		// 已过期的情况下，删除刷新令牌
		if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
			oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId());
			throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期");
		}

		// 创建访问令牌
		return createOAuth2AccessToken(refreshTokenDO, clientDO);
	}

	@Override
	public OAuth2AccessTokenDO getAccessToken(String accessToken) {
		// 优先从 Redis 中获取
		OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
		if (accessTokenDO != null) {
			return accessTokenDO;
		}

		// 获取不到，从 MySQL 中获取访问令牌
		accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
		if (accessTokenDO == null) {
			// 特殊：从 MySQL 中获取刷新令牌。原因：解决部分场景不方便刷新访问令牌场景
			// 例如说，积木报表只允许传递 token，不允许传递 refresh_token，导致无法刷新访问令牌
			// 再例如说，前端 WebSocket 的 token 直接跟在 url 上，无法传递 refresh_token
			OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(accessToken);
			if (refreshTokenDO != null && !DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
				accessTokenDO = convertToAccessToken(refreshTokenDO);
			}
		}

		// 如果在 MySQL 存在，则往 Redis 中写入
		if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
			oauth2AccessTokenRedisDAO.set(accessTokenDO);
		}
		return accessTokenDO;
	}

	@Override
	public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
		OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
		if (accessTokenDO == null) {
			throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在");
		}
		if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
			throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期");
		}
		return accessTokenDO;
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
		// 删除访问令牌
		OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
		if (accessTokenDO == null) {
			return null;
		}
		oauth2AccessTokenMapper.deleteById(accessTokenDO.getId());
		oauth2AccessTokenRedisDAO.delete(accessToken);
		// 删除刷新令牌
		oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
		return accessTokenDO;
	}

	@Override
	public PageResult<OAuth2AccessTokenDO> getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) {
		return oauth2AccessTokenMapper.selectPage(reqVO);
	}

	private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
		OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
			.setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
			.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()))
			.setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
			.setRefreshToken(refreshTokenDO.getRefreshToken())
			.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
		accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号，避免缓存到 Redis 的时候，无对应的租户编号
		oauth2AccessTokenMapper.insert(accessTokenDO);
		// 记录到 Redis 中
		oauth2AccessTokenRedisDAO.set(accessTokenDO);
		return accessTokenDO;
	}

	private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
		OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
			.setUserId(userId).setUserType(userType)
			.setClientId(clientDO.getClientId())
			//.setScopes(scopes)
			//jijun,25/9/6,修改框架
			.setScopes(clientDO.getScopes())
			.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
		oauth2RefreshTokenMapper.insert(refreshToken);
		return refreshToken;
	}

	private OAuth2AccessTokenDO convertToAccessToken(OAuth2RefreshTokenDO refreshTokenDO) {
		OAuth2AccessTokenDO accessTokenDO = BeanUtils.toBean(refreshTokenDO, OAuth2AccessTokenDO.class)
			.setAccessToken(refreshTokenDO.getRefreshToken());
		TenantUtils.execute(refreshTokenDO.getTenantId(),
			() -> accessTokenDO.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType())));
		return accessTokenDO;
	}

	/**
	 * 加载用户信息，方便 {@link com.snpic.appaw.framework.security.core.LoginUser} 获取到昵称、部门等信息
	 *
	 * @param userId   用户编号
	 * @param userType 用户类型
	 * @return 用户信息
	 */
	private Map<String, String> buildUserInfo(Long userId, Integer userType) {
		if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
			AdminUserDO user = adminUserService.getUser(userId);
			return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
				.put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(user.getDeptId())).build();
		} else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
			// 注意：目前 Member 暂时不读取，可以按需实现
			return Collections.emptyMap();
			//jijun,25/9/7,添加cclient
		}else if (userType.equals(UserTypeEnum.CCLIENT.getValue())) {
			return Collections.emptyMap();
		}
		return null;
	}

	private static String generateAccessToken() {
		return IdUtil.fastSimpleUUID();
	}

	private static String generateRefreshToken() {
		return IdUtil.fastSimpleUUID();
	}

}
